Skip to content

feat(rest): 端点条目进 OpenAPI enrichment;摘除 dispatcher 死分支(#5040 E6) - #5171

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5093-openapi-endpoints
Aug 4, 2026
Merged

feat(rest): 端点条目进 OpenAPI enrichment;摘除 dispatcher 死分支(#5040 E6)#5171
os-zhuang merged 2 commits into
mainfrom
claude/issue-5093-openapi-endpoints

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5093
Fixes #5078
Part-of #5040(E 系列第 6 单,按 2026-08-04 的 E6 设计修正执行,替代原设计 §3.5)

一句话

apis: 端点的文档面加入 rest-server 既有的 OpenAPI enrichment 管线(与 {object} 展开同根),同时摘除 http-dispatcher 里那条双重死的 generateOpenApi 探测分支、连同 route-ledger 里对应的失准行。现网行为零变更

为什么是 rest-server,而不是 MetadataManager.generateOpenApi

原设计 §3.5 推荐在 metadata service 上实现 generateOpenApi#5078 的真实 boot 把这条路否掉了:GET /api/v1/openapi.json 已经packages/rest 完整拥有并应答 —— 355KB 的 OpenAPI 3.1 文档,三个指纹全部指向 rest-server(servers[0] 按 Host 头注入、{object} 展开出 199 条 paths、恰好 2 条 x-template)。在 metadata service 上再实现一个,就是 ADR-0076 第 1 条明令禁止的第二属主——也正是本单存在的意义。E1 的契约成员因此已剔除。

所以本 PR 把文档面送进已有属主自己的管线,而不是新造一个生成器。

改了什么

1. packages/rest/src/openapi-endpoints.ts(新,纯函数)

每条声明贡献一个 path 条目:

出处 写进文档的东西
声明原样 path(原样)、method 小写作 Operation 键、operationId = name
冻结词表真有的文档字段 summary / description —— 缺省即缺省,不生成替身(生成的句子和作者写的句子事后分不出来)
执行器的既定事实(逐条注明出处) object_operationget/update/delete documented 一个必填的 id query 参数(词表无路径模板语法,requireRecordId 就是这么取的);create 答 201、其余 200;script / proxy 与缺 objectParamsobject_operation501

不编造任何 request / response schema。 出厂文档的 components.schemas 是空的(见下面越范围发现 #5168),此处再写 $ref 只会多六个悬空引用。请求体一律 type: object(「一个 JSON 对象,形状此处不描述」——这是真话),且只在 POST/PUT/PATCH 上出现。

authRequired → security:默认 true 由 schema parse 物化,为 true 的条目引用从文档自身读出的 security 方案(doc.security → 否则第一个 securitySchemes);在 rest 里硬写 bearerAuth 会造出第二处需要保持正确的地方,而且它失效时文档照样能解析、只是指向一个不存在的方案。为 false 的条目写显式 security: [] —— review 时一眼能看见的那个形状。

响亮跳过(与端点匹配器装载门同一姿态):不满足 ApiEndpointSchema 的条目点名跳过;同 method+path 撞车按「name 字典序在前者胜」裁决 —— 这是 buildEndpointIndex同一条规则,不这样文档就会指认一个运行时并不执行的端点;撞上内建 path+method 时内建保留、声明略过并报错(一路一主,在文档里同样成立)。

2. rest-server.ts:接进管线

{object} 展开与端点条目共用一次 protocol 解析,但各自一个 try —— 两者描述不同的面,一边枚举失败不该把另一边悄悄清空。

3. http-dispatcher.ts:摘除死分支

generateOpenApi 作为方法在本仓与两个兄弟仓零实现(同名命中只剩一个无关的 config 布尔键),且 boot 实测没有任何路由/openapi.json 送进 dispatch() —— 双重死。删除,原地留注明出处的注释。

4. route-ledger.ts:台账修真

⚠️ 与卡面 scope 的一处偏差,请复核:卡面写「修正 GET /openapi.json 行的注记」,本 PR 删除了该行(并从 LEGACY_CHAIN_PREFIXES 移除 /openapi.json),两处都留了注明出处的注释。理由:

原注记「falls through when metadata service lacks a generator」读起来像有时有有时没有;实际从来没有,是 100% fall-through 穿了件条件句的外衣 —— 这就是 #5078 立单的那个点。

空集不变量(本单最吃劲的测试)

publish/validate 对非空 apis: 仍然硬拒(E7 前不撤),所以今天枚举出的是空集,本单必须在这个状态下完全不可见。三层钉住:

  1. enrichOpenApiWithEndpoints(doc, []) 返回同一个对象(toBe),不是等价对象 —— 字节相同由构造保证;
  2. 全部条目解析失败时同样返回同一个对象(退化的中间态);
  3. 路由级:同一个 handler,「协议有 api 类型但返回空集」与「协议根本不支持 api 枚举」(= 本改动前的世界)两份响应 JSON.stringify 逐字节相等。

外加「加了端点时不得改动 base spec」(base spec 跨请求缓存,一次污染就会漏进之后每个响应)。

验证

pnpm --filter @objectstack/rest test        → 40 files / 608 tests passed(新增 31)
pnpm --filter @objectstack/runtime test     → 88 files / 1270 tests passed
pnpm --filter @objectstack/runtime typecheck→ clean(tsc --noEmit 无输出)
npx tsc --noEmit -p packages/rest           → 仅 2 条既有错误,均在未触碰的 package-routes.ts
eslint(6 个改动文件)                        → clean
pnpm --filter @objectstack/spec check:generated → ✓ All 8 generated artifacts are up to date
check:nul-bytes / route-envelope / wildcard-fallthrough / adr-anchors
  / error-code-casing / type-check-coverage → 全绿
git diff --numstat                           → 无 `- -`(零二进制条目)

越范围发现(已立单,未在本 PR 修)

#5168 —— 发布出去的 OpenAPI 文档 components.schemas 恒为空,而 paths 里 6 个 $ref 全部悬空(覆盖 CRUD 全部请求/响应体)。根因已用对照实验坐实:build-openapi.ts 的收集判据 typeof schema === 'object',而这些 schema 经 lazySchema() 包装后 Proxy target 是函数,typeof'function' —— OS_EAGER_SCHEMAS=1 下同一条命令输出 Components: 9,默认输出 Components: 0gen:openapi 是全仓两个完全无门禁的生成器之一,所以三层不自洽(空 components、悬空 ref、日志里明晃晃的 Components: 0)一处都没红。本单因此不 $ref 任何 components。

未做


🤖 Generated with Claude Code

https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd


Generated by Claude Code

claude added 2 commits August 4, 2026 07:28
…5040 E6)

`GET {basePath}/openapi.json` has one owner and a real boot proved it is
`packages/rest` (#5078: a 355KB OpenAPI 3.1 document with a Host-injected
`servers[0]`, 199 paths after `{object}` expansion and two `x-template`
markers — every one a rest-server fingerprint). So the documentation face for
`apis:` endpoints joins that pipeline instead of a `generateOpenApi` on a
metadata service, which would have been the second owner ADR-0076 forbids.

Each declaration contributes its literal `path`, the lower-cased method as the
Operation key, `operationId` = `name`, and the only two documentation fields
the frozen vocabulary carries (`summary` / `description`, omitted when absent
rather than replaced by a generated stand-in). Everything else emitted is a
fact about how the executor treats the declaration, each cited to its
authority: the `query.id` parameter for object_operation get/update/delete,
201 for create and 200 otherwise, and 501 for script / proxy / an
object_operation missing its `objectParams`. No invented request or response
schemas — the shipped document's `components.schemas` is empty (#5168), so an
emitted `$ref` would dangle like the six built-in ones already do.

`authRequired` is materialised by the parse, so authenticated operations point
at a security scheme read off the document itself (not a scheme name
hard-coded in rest, which would be a second place to keep right) and
`authRequired: false` emits an explicit `security: []`. Items failing
`ApiEndpointSchema` are skipped loudly and named; a duplicate method+path is
resolved by lexicographically-first `name`, the endpoint matcher's own rule,
so the document cannot name an endpoint the runtime does not run; a
declaration never displaces a built-in path+method.

Live behaviour is unchanged: publish still rejects a non-empty `apis:` until
the E7 flip, so the enumeration is empty and the enricher returns its input
document by reference — pinned by tests rather than argued.

Refs #5093, #5040

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
…s ledger row (#5078)

The dispatcher carried a `GET /openapi.json` branch that duck-typed a
`generateOpenApi` method on the metadata service. No implementation has ever
provided it — not `MetadataManager`, not `NodeMetadataManager`, not a plugin,
not either sibling repo; the only other repo-wide hits for the name are an
unrelated boolean config key and its tests. The `if` was constant-false on
every request ever served.

It was dead a second way, which is what settles the disposition rather than
merely arguing it: the real boot in #5078 showed nothing routes
`/openapi.json` into `dispatch()` at all. `packages/rest` owns and answers the
path end to end, and there is no ownership contest to resolve — only a shadow
to remove (ADR-0076 "one route, one owner": a second implementation of a path
another package already serves is code `grep` finds and the runtime never
runs).

The ledger moves with it. The row's note said the path "falls through when
metadata service lacks a generator", which reads as sometimes-yes; it was
always, so the note was the machine-readable surface ADR-0076 §4 forbids —
that inaccuracy is exactly what #5078 was filed about. The row is removed
rather than reworded: this ledger enumerates the routes THIS package's
dispatcher serves, and it now serves none there. `/openapi.json` leaves
`LEGACY_CHAIN_PREFIXES` for the same reason — that list means "branches of the
`dispatch()` if-chain", and keeping a prefix in it for a branch that no longer
exists would tell the same class of lie in the very change that stops one.
Cited comments stay at both sites pointing at the real owner and its truthful
row in `packages/rest/src/rest-route-ledger.ts`.

Refs #5093, #5040

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 7:29am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/runtime.

26 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/releases/v12.mdx (via @objectstack/rest)
  • content/docs/releases/v17.mdx (via @objectstack/rest, @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review August 4, 2026 07:30
@os-zhuang
os-zhuang enabled auto-merge August 4, 2026 07:30
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 1203bb2 Aug 4, 2026
24 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5093-openapi-endpoints branch August 4, 2026 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants